package com.geek.bulk;

import org.eclipse.microprofile.faulttolerance.Retry;

import java.time.temporal.ChronoUnit;
import java.util.concurrent.TimeUnit;

import static com.geek.bulk.util.ClassUtils.isDerived;
import static com.geek.bulk.util.TimeUtils.toTimeUnit;
import static java.lang.String.format;


public class Retryer {

    private int maxRetries;

    private long delay;

    private ChronoUnit delayUnit;

    private long maxDuration;

    private ChronoUnit durationUnit;

    private long jitter;

    private ChronoUnit jitterDelayUnit;

    private Class<? extends Throwable>[] retryOn;

    private Class<? extends Throwable>[] abortOn;


    private int attempt;


    public static Retryer build(Retry retry) {
        return new Retryer()
                .setDelay(retry.delay())
                .setDelayUnit(retry.delayUnit())
                .setMaxDuration(retry.maxDuration())
                .setMaxRetries(retry.maxRetries())
                .setDurationUnit(retry.durationUnit())
                .setJitter(retry.jitter())
                .setJitterDelayUnit(retry.jitterDelayUnit())
                .setRetryOn(retry.retryOn())
                .setAbortOn(retry.abortOn())
                .setAttempt(0);
    }

    public void continueOrPropagate(Exception e) throws Exception {
        //判断是否应该重试   e 是被InvocationTargetException包了一层
        if (!isRetryOn(e.getCause()) || isAbortOn(e.getCause())) {
            throw e;
        }

        if (++attempt >= maxRetries) {
            throw e;
        }
        //延时多久执行下次
        long optionalDelay = optionalDelay();
        //最大等待时间
        long maxDuration = optionalMaxDuration();

        if (optionalDelay >= maxDuration) {
            throw new IllegalArgumentException(
                    format("The max duration[%d ns] must be greater than the delay duration[%d ns] if set.",
                            optionalDelay, maxDuration));
        }

        //设置重试间隔
        long jetter = optionalJetter();
        try {
            //这里是纳秒
            TimeUnit.NANOSECONDS.sleep(jetter + optionalDelay);
        } catch (InterruptedException ignored) {
            Thread.currentThread().interrupt();
            throw e;
        }

    }

    private boolean isRetryOn(Throwable e) {
        return isDerived(e.getClass(), retryOn);
    }

    private boolean isAbortOn(Throwable e) {
        return isDerived(e.getClass(), abortOn);
    }

    private long optionalJetter() {
        if (jitter < 1) {
            return jitter;
        }
        TimeUnit timeUnit = toTimeUnit(jitterDelayUnit);
        return timeUnit.toNanos(jitter);
    }

    private long optionalMaxDuration() {
        if (maxDuration < 1) {
            return maxDuration;
        }
        TimeUnit timeUnit = toTimeUnit(durationUnit);
        return timeUnit.toNanos(maxDuration);
    }


    public long optionalDelay() {
        if (delay < 1) {
            return delay;
        }
        TimeUnit timeUnit = toTimeUnit(delayUnit);
        return timeUnit.toNanos(delay);
    }

    public int getMaxRetries() {
        return maxRetries;
    }

    public Retryer setMaxRetries(int maxRetries) {
        this.maxRetries = maxRetries;
        return this;
    }

    public long getDelay() {
        return delay;
    }

    public Retryer setDelay(long delay) {
        this.delay = delay;
        return this;
    }

    public ChronoUnit getDelayUnit() {
        return delayUnit;
    }

    public Retryer setDelayUnit(ChronoUnit delayUnit) {
        this.delayUnit = delayUnit;
        return this;
    }

    public long getMaxDuration() {
        return maxDuration;
    }

    public Retryer setMaxDuration(long maxDuration) {
        this.maxDuration = maxDuration;
        return this;
    }

    public ChronoUnit getDurationUnit() {
        return durationUnit;
    }

    public Retryer setDurationUnit(ChronoUnit durationUnit) {
        this.durationUnit = durationUnit;
        return this;
    }

    public long getJitter() {
        return jitter;
    }

    public Retryer setJitter(long jitter) {
        this.jitter = jitter;
        return this;
    }

    public ChronoUnit getJitterDelayUnit() {
        return jitterDelayUnit;
    }

    public Retryer setJitterDelayUnit(ChronoUnit jitterDelayUnit) {
        this.jitterDelayUnit = jitterDelayUnit;
        return this;
    }

    public Class<? extends Throwable>[] getRetryOn() {
        return retryOn;
    }

    public Retryer setRetryOn(Class<? extends Throwable>[] retryOn) {
        this.retryOn = retryOn;
        return this;
    }

    public Class<? extends Throwable>[] getAbortOn() {
        return abortOn;
    }

    public Retryer setAbortOn(Class<? extends Throwable>[] abortOn) {
        this.abortOn = abortOn;
        return this;
    }


    public int getAttempt() {
        return attempt;
    }

    public Retryer setAttempt(int attempt) {
        this.attempt = attempt;
        return this;
    }

}
